/**
 * Copyright 2013 Douglas Lacher
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @author Douglas Lacher <doug.lacher@gmail.com>
 */

package com.devioustoast.andmoco;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import android.util.Log;
import android.util.Xml;

/**
 * This class contains static methods for parsing the moves files from XML.
 * Many methods here came from http://developer.android.com/training/basics/network-ops/xml.html.
 *
 * @author Douglas Lacher <doug.lacher@gmail.com>
 */
public class MoveParser {

  public static final String LOG_TAG = "MoveParser";

  private static final String XML_NAME_SPACE = null;

  private MoveParser() { }

  /**
   * Parses an XML file representing a "Move" object.
   *
   * @param inputStream an open InputStream object.
   * @param robot a Robot with Legs and Servos.
   * @return a Move.
   * @throws XmlPullParserException if the XML could not be parsed.
   * @throws IOException if the XML could not be parsed.
   */
  public static Move parseMoveFile(InputStream inputStream, Robot robot)
      throws XmlPullParserException, IOException {
    InputStreamReader reader = new InputStreamReader(inputStream);
    XmlPullParser parser = Xml.newPullParser();
    parser.setInput(reader);
    return parseMoveXml(parser, robot);
  }

  /**
   * Parses an XML file representing a "Move" object.
   *
   * @param parser an open parser handle.
   * @param robot a Robot with Legs and Servos.
   * @return a Move.
   * @throws XmlPullParserException if the XML could not be parsed.
   * @throws IOException if the XML could not be parsed.
   */
  public static Move parseMoveXml(XmlPullParser parser, Robot robot)
      throws XmlPullParserException, IOException {
    try {
      parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
    } catch (XmlPullParserException e) {
      Log.d(
          LOG_TAG,
          "Could not disable namespace processing.  Is it supported?  Continuing anyway.", e);
    }
    parser.nextTag(); // Required to start the parser.
    return parseMove(parser, robot);
  }

  /**
   * Parses an XML <move> element.
   *
   * @param parser an open parser handle.
   * @param robot a Robot with Legs and Servos.
   * @return a Move.
   * @throws XmlPullParserException
   * @throws IOException
   */
  private static Move parseMove(XmlPullParser parser, Robot robot)
      throws XmlPullParserException, IOException {
    Move move = new Move();
    parser.require(XmlPullParser.START_TAG, XML_NAME_SPACE, "move");
    while (parser.next() != XmlPullParser.END_TAG) {
      if (parser.getEventType() != XmlPullParser.START_TAG) {
        continue;
      }
      String name = parser.getName();
      if (name.equals("name")) {
        move.name = readText(parser);
      } else if (name.equals("movement")) {
        move.movements.add(parseMovement(parser, robot));
      } else if (name.equals("wait")) {
        move.movements.add(parseWaitMovement(parser, robot));
      } else if (name.equals("repeat")) {
        move.movements.addAll(parseRepeatedMovement(parser, robot));
      } else if (name.equals("replant")) {
        move.movements.addAll(parseReplantMovement(parser, robot));
      } else {
        Log.w(LOG_TAG, String.format("Skipping unknown XML tag \"%s\".", name));
        skip(parser);
      }
    }
    parser.require(XmlPullParser.END_TAG, XML_NAME_SPACE, "move");
    return move;
  }

  /**
   * Parses an XML <repeat> element.  <repeat> elements may have a "times" attribute with an
   * integer value specifying how many times to run the enclosed element(s).  If no "times"
   * attribute exists, then the enclosed elements are repeated once (equivalent to times=2).  A
   * "times" value less than 2 is invalid and replaced with 2.
   *
   * @param parser an open parser handle.
   * @param robot a Robot with Legs and Servos.
   * @return a List of Movements.
   * @throws XmlPullParserException
   * @throws IOException
   */
  private static List<Movement> parseRepeatedMovement(XmlPullParser parser, Robot robot)
      throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, XML_NAME_SPACE, "repeat");
    List<Movement> repeatedMovements = new ArrayList<Movement>();
    int times = 2;
    String rawTimes = parser.getAttributeValue(XML_NAME_SPACE, "times");
    if (rawTimes != null) {
      try {
        times = Integer.valueOf(rawTimes);
        if (times < 2) {
          Log.e(LOG_TAG, String.format("Attribute times=\"%s\" is invalid.", rawTimes));
          times = 2;
        }
      } catch (NumberFormatException e) {
        Log.e(LOG_TAG, String.format("Attribute times=\"%s\" is invalid.", rawTimes));
      }
    }
    List<Movement> movements = new ArrayList<Movement>();
    while (parser.next() != XmlPullParser.END_TAG) {
      if (parser.getEventType() != XmlPullParser.START_TAG) {
        continue;
      }
      String name = parser.getName();
      if (name.equals("movement")) {
        movements.add(parseMovement(parser, robot));
      } else if (name.equals("wait")) {
        movements.add(parseWaitMovement(parser, robot));
      } else if (name.equals("repeat")) {
        movements.addAll(parseRepeatedMovement(parser, robot));
      } else if (name.equals("replant")) {
        movements.addAll(parseReplantMovement(parser, robot));
      } else {
        Log.w(LOG_TAG, String.format("Skipping unknown XML tag \"%s\".", name));
        skip(parser);
      }
    }
    for (int i = 0; i < times; i++) {
      repeatedMovements.addAll(movements);
    }
    parser.require(XmlPullParser.END_TAG, XML_NAME_SPACE, "repeat");
    return repeatedMovements;
  }

  /**
   * Parses an XML <movement> element.
   *
   * @param parser an open parser handle.
   * @param robot a Robot with Legs and Servos.
   * @return a Movement.
   * @throws XmlPullParserException
   * @throws IOException
   */
  private static Movement parseMovement(XmlPullParser parser, Robot robot)
      throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, XML_NAME_SPACE, "movement");
    Servo servo = null;
    Integer microseconds = null;
    Float degrees = null;
    Boolean relative = false;
    Integer wait = null;
    while (parser.next() != XmlPullParser.END_TAG) {
      if (parser.getEventType() != XmlPullParser.START_TAG) {
        continue;
      }
      String name = parser.getName();
      if (name.toLowerCase(Locale.US).equals("servo")) {
        String legName = readText(parser);
        servo = parseLegAndJoint(legName, robot);
      } else if (name.toLowerCase(Locale.US).equals("microseconds")) {
        microseconds = Integer.valueOf(readText(parser));
      } else if (name.toLowerCase(Locale.US).equals("degrees")) {
        degrees = Float.valueOf(readText(parser));
      } else if (name.toLowerCase(Locale.US).equals("relative")) {
        relative = Boolean.valueOf(readText(parser));
      } else if (name.toLowerCase(Locale.US).equals("wait")) {
        wait = Integer.valueOf(readText(parser));
      } else {
        Log.d(LOG_TAG, String.format("Skipping tag \"%s\".", name));
        skip(parser);
      }
    }
    Movement movement = null;
    if (microseconds != null) {
      movement = new Movement(servo, microseconds, relative, wait);
    } else if (degrees != null) {
      movement = new Movement(servo, degrees, relative, wait);
    }
    parser.require(XmlPullParser.END_TAG, XML_NAME_SPACE, "movement");
    return movement;
  }

  /**
   * Parses an XML <wait> element if found outside a <movement> element.
   *
   * @param parser an open parser handle.
   * @param robot a Robot with Legs and Servos.
   * @return a Movement.
   * @throws XmlPullParserException
   * @throws IOException
   */
  private static Movement parseWaitMovement(XmlPullParser parser, Robot robot)
      throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, XML_NAME_SPACE, "wait");
    Movement movement = new Movement(Integer.valueOf(readText(parser)));
    parser.require(XmlPullParser.END_TAG, XML_NAME_SPACE, "wait");
    return movement;
  }

  private static List<Movement> parseReplantMovement(XmlPullParser parser, Robot robot)
      throws XmlPullParserException, IOException {
    parser.require(XmlPullParser.START_TAG, XML_NAME_SPACE, "replant");
    Leg leg = null;
    Float degrees = null;
    Integer microseconds = null;
    Boolean relative = false;
    Integer wait = null;
    while (parser.next() != XmlPullParser.END_TAG) {
      if (parser.getEventType() != XmlPullParser.START_TAG) {
        continue;
      }
      String name = parser.getName();
      if (name.toLowerCase(Locale.US).equals("leg")) {
        leg = robot.getLeg(parseLeg(readText(parser)));
      } else if (name.toLowerCase(Locale.US).equals("microseconds")) {
        microseconds = parseInteger(readText(parser));
      } else if (name.toLowerCase(Locale.US).equals("degrees")) {
        degrees = parseFloat(readText(parser));
      } else if (name.toLowerCase(Locale.US).equals("relative")) {
        relative = parseBoolean(readText(parser));
      } else if (name.toLowerCase(Locale.US).equals("wait")) {
        wait = parseInteger(readText(parser));
      } else {
        Log.w(LOG_TAG, String.format("Skipping unknown XML tag \"%s\".", name));
        skip(parser);
      }
    }
    if (leg == null || (degrees == null && microseconds == null)) {
      throw new XmlPullParserException(
          "Invalid replant movement.  A leg and one of degrees or microseconds must be provided.");
    }

    List<Movement> movements = new ArrayList<Movement>();
    movements.add(new Movement(leg.knee, -30, true));
    if (degrees != null) {
      movements.add(new Movement(leg.hip, degrees, relative));
    } else if (microseconds != null) {
      movements.add(new Movement(leg.hip, microseconds, relative));
    }
    movements.add(new Movement(leg.knee, 30, true));
    if (wait != null) {
      movements.add(new Movement(wait));
    }
    parser.require(XmlPullParser.END_TAG, XML_NAME_SPACE, "replant");
    return movements;
  }

  /**
   * Reads a text element from the XML parser.
   *
   * @param parser an open parser handle.
   * @return a String containing what was read.
   * @throws IOException
   * @throws XmlPullParserException
   */
  private static String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
    String result = "";
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.getText();
        parser.nextTag();
    }
    return result;
  }

  /**
   * Parses a leg and joint name and returns the corresponding servo from the Robot.
   *
   * Leg and joint names are expected to be formatted in all capitals with a period in between
   * (e.g. "LF.ANKLE") except for the neck, which is labeled simple as "NECK".
   *
   * Supported leg names are enumerated in LegName.  This includes "NECK" for the neck servo.
   * Supported joint names are enumerated in JointName.
   *
   * @param legAndJoint a LegName and JointName separated by a period or a servo name.
   * @param robot a Robot with Legs and Servos.
   * @return a Servo from the Robot corresponding to the input's name.
   */
  private static Servo parseLegAndJoint(String legAndJoint, Robot robot) {
    LegName legName = null;
    JointName jointName = null;
    Leg leg = null;
    Servo servo = null;
    String[] parts = legAndJoint.split("\\.");
    if (parts.length == 1 && LegName.NECK == LegName.valueOf(parts[0])) {
      servo = robot.neck;
    } else if (parts.length == 2) {
      try {
        legName = LegName.valueOf(parts[0]);
        jointName = JointName.valueOf(parts[1]);
        leg = robot.getLeg(legName);
        if (JointName.HIP == jointName) {
          servo = leg.hip;
        } else if (JointName.KNEE == jointName) {
          servo = leg.knee;
        } else if (JointName.ANKLE == jointName) {
          servo = leg.ankle;
        }
      } catch (IllegalArgumentException e) {
        Log.e(LOG_TAG, String.format("Invalid leg and joint name, \"%s\".", legAndJoint));
      }
    } else {
      Log.e(LOG_TAG, String.format("Invalid leg and joint format, \"%s\".", legAndJoint));
    }
    return servo;
  }

  /**
   * Parses a leg name and returns a LegName.
   *
   * @param leg the String representation of the LegName.
   * @return a LegName or null if it could not be parsed.
   */
  private static LegName parseLeg(String leg) {
    try {
      return LegName.valueOf(leg);
    } catch (IllegalArgumentException e) {
      Log.e(LOG_TAG, String.format(
          "Invalid leg name, \"%s\".  Must be one of %s.", leg, Arrays.asList(LegName.values())));
    }
    return null;
  }

  /**
   * Parses a floating point number from a String and returns a Float.
   *
   * @param f the String representation of the Float.
   * @return a Float or null if it could not be parsed.
   */
  private static Float parseFloat(String f) {
    try {
      return Float.valueOf(f);
    } catch (IllegalArgumentException e) {
      Log.e(LOG_TAG, String.format("Invalid Float value, \"%s\".  Must be a float.", f));
    }
    return null;
  }

  /**
   * Parses an integer number from a String and returns a Float.
   *
   * @param i the String representation of the Integer.
   * @return an Integer or null if it could not be parsed
   */
  private static Integer parseInteger(String i) {
    try {
      return Integer.valueOf(i);
    } catch (IllegalArgumentException e) {
      Log.e(LOG_TAG, String.format("Invalid Integer value, \"%s\".  Must be an integer.", i));
    }
    return null;
  }

  /**
   * Parses a boolean value from a String and returns a Boolean.
   *
   * @param b the String representation of the Boolean.
   * @return a Boolean or null if it could not be parsed.
   */
  private static Boolean parseBoolean(String b) {
    try {
      return Boolean.valueOf(b);
    } catch (IllegalArgumentException e) {
      Log.e(LOG_TAG, String.format("Invalid Boolean value, \"%s\".  Must be true or false.", b));
    }
    return null;
  }

  /**
   * Skips the current tag and its children.
   *
   * @param parser an open parser handle.
   * @throws XmlPullParserException
   * @throws IOException
   */
  private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
    if (parser.getEventType() != XmlPullParser.START_TAG) {
        throw new IllegalStateException();
    }
    int depth = 1;
    while (depth != 0) {
        switch (parser.next()) {
        case XmlPullParser.END_TAG:
            depth--;
            break;
        case XmlPullParser.START_TAG:
            depth++;
            break;
        }
    }
 }
}
